DirectDraw - Drawing Stuff!




Hello, and welcome to the second tutorial in the DirectDraw series. In this tutorial, I'll show you how to actually draw stuff with DirectDraw. By the end of this tutorial, we'll be plotting pixels all over the place! Consequently, since all we're doing is plotting pixels, this will be a short tutorial, with lots of tasks at the end.

Okay, first of all, I'm going to start with a little background on how DirectDraw Surfaces actually work. They are basically pointers to a regularly spaced segment in memory that represents the screen. All you have to do is find out where that segment starts and how much space there is between pixels and you can put a pixel anywhere.
However, there is a problem with this, but a simple one. DirectDraw requires you to perform a task called "locking" the surfaces before you plot a pixel and "unlock" them when you are done. This is to tell DirectDraw that you are accessing the surfaces directly at the memory level so it won't mess with them at the same time.
Here is how you "lock" and "unlock" a surface:
HRESULT LPDIRECTDRAWSURFACE->Lock(LPRECT lpDestRect, LPDDSURFACEDESC lpDDSurfaceDesc, DWORD dwFlags, HANDLE hEvent);
HRESULT LPDIRECTDRAWSURFACE->Unlock(LPRECT lpDestRect);

As you can see, this is a member function of the surface itself. In both, there is an LPRECT parameter specifying which part you want to lock. If you want to lock the whole thing, which you most often will, just pass NULL. The LPDDSURFACEDESC is the surface description that the function will fill in and we can use to plot pixels. The dwFlags are lock description flags that you will probably notice from earlier functions. These are quite common in DirectX. The last part, hEvent, you can simply set to NULL, as this is an advanced parameter.
Now I will show you how to actually lock and unlock both the secondary surface.
// Fill in the surface description
memset(&ddsd, 0, sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
// Lock the secondary surface
DDReturnValue = lpddssecondary->Lock(NULL, &ddsd, DDLOCK_SUFACEMEMORYPTR | DDLOCK_WAIT, NULL);
// Unlock it
DDReturnValue = lpddssecondary->Unlock(NULL);

The code to lock the primary surface is basically identical, simply with lpddsprimary instead of lpddssecondary. You can lock both at the same time, but you need to use a different DDSURFACEDESC variable, and since we only have 1 declared right now, I simply only locked one. The two flags, DDLOCK_SURFACEMEMORYPTR and DDLOCK_WAIT are used to tell DirectDraw that we want a pointer to this surface memory and that we will wait if we can't lock the surface now.
Now we're going to go about plotting a pixel, which is slightly more different. We need an unsigned int pointer to tell us where our memory starts. Now in this next part, x and y represent where on the screen we're going to put this pixel. R, G, and B represent the RGB color values of this pixel. I'm assuming we're using 16-bit or higher color. You can use 8-bit palettized mode, but why would you? Oh, and by the way, assume that x, y, R, G, and B are already declared and given a value somewhere else. I'll leave what those values should be up to you. Here you go:
// Unsigned integer pointer to memory
UINT * video_buffer;
// Get the location of the surface from ddsd
video_buffer = (UINT *)ddsd.lpSurface;
// Plot the pixel
video_buffer[x + (y * ddsd.lPitch >> 1)] = (UINT)RGB(R,G,B);
// Clean up
video_buffer = NULL;

The first part is self explanatory. Next we take the pointer to our surface from the DD surface description structure and convert that to an unsigned integer. The next line will take the most explanation. We first recognize that this pointer to a memory segment acts like an array of memory spots. We can find which spot to put the pixel in by using x and y. We start with the x position across and add in the y position mulitplied by the vertical pitch of the surface. The bit shift is because we have the pitch in UCHARs and we want it in UINTs, so we divide by 2 to convert the two. This spot is set equal to the color of our pixel, or the RGB components made into an unsigned integer by the RGB() macro. On the last line we simply clean up our memory pointer so that we don't accidently write to it later.

Now, at this point you will notice that if you have been using lpddsprimary as your surface, you see pixels drawing themselves on your screen. If you have instead been using lpddssecondary, as I showed you, all you see is black. That is because we're writing to an offscreen surface, not the screen itself, so we need to flip the backbuffer to the screen. Here's the prototype for that:
HRESULT LPDIRECTDRAWSURFACE->Flip(LPDIRECTDRAWSURFACE lpDDSurfaceOverride, DWORD dwFlags);

This function flips the backbuffer onto the screen so it's visible. Basically it's the equivalent of a very fast memcpy function taking into account other DirectDraw factors. And, if you were wondering, both your primary surface and backbuffer MUST be unlocked for this to work:
while(lpddsprimary->Flip (NULL, DDFLIP_WAIT) != DD_OK);

This tells DirectDraw that we want to flip the default backbuffer (lpddssecondary, since we passed NULL) onto the screen and that we're willing to wait for it to happen instead of it failing and not flipping at all. The while statement is in there to make sure that it works and doesn't just error out on us. :-)

Well, that's about it for drawing pixels. We now have a program that can draw pixels to the screen, or use the backbuffer to do double-buffered pixel drawing. This isn't particularly useful now, but soon you will see why we would want to do this.

Tasks for this tutorial:
1) Put all this code into a function so that you can call it will a position and color and it will plot the pixel for you.
2) Create a program to fill the screen with random pixels of different colors and positions.
3) Create a function that will draw a circle of pixels on the screen with a given center, radius and color. Hint: use the sin() and cos() functions inside math.h